/*
 * The MIT License
 * Copyright (c) 2012 kc4.org
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.kc4.sso.crypto;

import java.io.Serializable;
import java.nio.charset.Charset;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.spec.X509EncodedKeySpec;

import javax.crypto.Cipher;

import org.kc4.util.Hex;

/**
 * RSA による、非対称鍵暗号器。
 *
 * @author yoichi kikuchi
 */
public class SSOAsymmetricCipher implements Serializable {

	private static final long serialVersionUID = 1L;

	private static final String ALGORITHM = "RSA";

	private static final String CLEARTEXT_CHARSET = "UTF-8";

	private KeyPair keyPair;
	private PublicKey publickey;

	/**
	 * 非対称鍵暗号器を生成します。
	 * インスタンス生成時にキーペアが生成されます。
	 */
	public SSOAsymmetricCipher() {

		this.setKeyPair(this.newKeyPair());
	}

	/**
	 * 非対称鍵暗号器を生成します。
	 *
	 * @param publickey 16進数文字列形式の公開鍵。
	 */
	public SSOAsymmetricCipher(String publickey) {

		this.setPublickey(publickey);
	}

	/**
	 * 平文を署名します。
	 *
	 * @param cleartext 16進数文字列形式の平文。
	 * @return 16進数文字列形式の署名。
	 */
	public String signAsHex(String cleartext) {

		return new Hex(this.sign(cleartext)).toString();
	}

	/**
	 * 平文を署名します。
	 *
	 * @param cleartext 平文。
	 * @return 16進数文字列形式の署名。
	 */
	public String signAsHex(byte[] cleartext) {

		return new Hex(this.sign(cleartext)).toString();
	}

	/**
	 * 平文を署名します。
	 *
	 * @param cleartext 16進数文字列形式の平文。
	 * @return 署名。
	 */
	public byte[] sign(String cleartext) {

		return this.sign(new Hex(cleartext).toBytes());
	}

	/**
	 * 平文を署名します。
	 *
	 * @param cleartext 平文。
	 * @return 署名。
	 */
	public byte[] sign(byte[] cleartext) {

		try {

			Cipher asymmetric = Cipher.getInstance(ALGORITHM);
			asymmetric.init(Cipher.ENCRYPT_MODE, this.getKeyPair().getPrivate());
			return asymmetric.doFinal(cleartext);
		}
		catch (Exception e) {

			throw new IllegalStateException(e);
		}
	}

	/**
	 * 署名を確認します。
	 *
	 * @param encrypted 16進数文字列形式の署名。
	 * @param cleartext 16進数文字列形式の平文。
	 * @return 署名の妥当性が確認できた場合は true、それ以外の場合は false。
	 */
	public boolean certify(String encrypted, String cleartext) {

		return this.certify(new Hex(encrypted).toBytes(), cleartext);
	}

	/**
	 * 署名を確認します。
	 *
	 * @param encrypted 署名。
	 * @param cleartext 平文。
	 * @return 署名の妥当性が確認できた場合は true、それ以外の場合は false。
	 */
	public boolean certify(byte[] encrypted, String cleartext) {

		try {

			Cipher asymmetric = Cipher.getInstance(ALGORITHM);
			asymmetric.init(Cipher.DECRYPT_MODE, this.getPublickey());
			return new Hex(asymmetric.doFinal(encrypted)).toString().equals(cleartext);
		}
		catch (Exception e) {

			throw new IllegalStateException(e);
		}
	}

	/**
	 * 平文を暗号化して、16進数文字列形式で取得します。
	 *
	 * @param cleartext 16進数文字列形式の平文。
	 * @return 暗号文。
	 */
	public String encryptAsHex(String cleartext) {

		return new Hex(this.encrypt(cleartext)).toString();
	}

	/**
	 * 平文を暗号化して、16進数文字列形式で取得します。
	 *
	 * @param cleartext 平文。
	 * @return 暗号文。
	 */
	public String encryptAsHex(byte[] cleartext) {

		return new Hex(this.encrypt(cleartext)).toString();
	}

	/**
	 * 平文を暗号化します。
	 *
	 * @param cleartext 16進数文字列形式の平文。
	 * @return 暗号文。
	 */
	public byte[] encrypt(String cleartext) {

		return this.encrypt(new Hex(cleartext).toBytes());
	}

	/**
	 * 平文を暗号化します。
	 *
	 * @param cleartext 平文。
	 * @return 暗号文。
	 */
	public byte[] encrypt(byte[] cleartext) {

		try {

			Cipher asymmetric = Cipher.getInstance(ALGORITHM);
			asymmetric.init(Cipher.ENCRYPT_MODE, this.getPublickey());
			return asymmetric.doFinal(cleartext);
		}
		catch (Exception e) {

			throw new IllegalStateException(e);
		}

	}

	/**
	 * 暗号文を復号します。
	 * 暗号文のもととなった平文の文字セットは、UTF-8 である必要があります。
	 *
	 * @param encrypted 16進数文字列形式の暗号文。
	 * @return 文字列形式の平文。
	 */
	public String decryptAsString(String encrypted) {

		return this.decryptAsString(encrypted, Charset.forName(CLEARTEXT_CHARSET));
	}

	/**
	 * 暗号文を復号します。
	 * 暗号文のもととなった平文の文字セットは、UTF-8 である必要があります。
	 *
	 * @param encrypted 暗号文。
	 * @return 文字列形式の平文。
	 */
	public String decryptAsString(byte[] encrypted) {

		return this.decryptAsString(encrypted, Charset.forName(CLEARTEXT_CHARSET));
	}

	/**
	 * 暗号文を復号します。
	 *
	 * @param encrypted 16進数文字列形式の暗号文。
	 * @param charset 平文の文字セット。
	 * @return 平文。
	 */
	public String decryptAsString(String encrypted, Charset charset) {

		return new String(this.decrypt(encrypted), charset);
	}

	/**
	 * 暗号文を復号します。
	 *
	 * @param encrypted 暗号文。
	 * @param charset 平文の文字セット。
	 * @return 平文。
	 */
	public String decryptAsString(byte[] encrypted, Charset charset) {

		return new String(this.decrypt(encrypted), charset);
	}

	/**
	 * 暗号文を復号します。
	 *
	 * @param encrypted 16進数文字列形式の暗号文。
	 * @return 16進数文字列形式の平文。
	 */
	public String decryptAsHex(String encrypted) {

		return new Hex(this.decrypt(encrypted)).toString();
	}

	/**
	 * 暗号文を復号します。
	 *
	 * @param encrypted 暗号文。
	 * @return 16進数文字列形式の平文。
	 */
	public String decryptAsHex(byte[] encrypted) {

		return new Hex(this.decrypt(encrypted)).toString();
	}

	/**
	 * 暗号文を復号します。
	 *
	 * @param encrypted 16進数文字列形式の暗号文。
	 * @return 平文。
	 */
	public byte[] decrypt(String encrypted) {

		return this.decrypt(new Hex(encrypted).toBytes());
	}

	/**
	 * 暗号文を復号します。
	 *
	 * @param encrypted 暗号文。
	 * @return 平文。
	 */
	public byte[] decrypt(byte[] encrypted) {

		try {

			Cipher asymmetric = Cipher.getInstance(ALGORITHM);
			asymmetric.init(Cipher.DECRYPT_MODE, this.getKeyPair().getPrivate());
			return asymmetric.doFinal(encrypted);
		}
		catch (Exception e) {

			throw new IllegalStateException(e);
		}
	}

	/**
	 * キーペアを生成します。
	 *
	 * @return キーペア。
	 */
	protected KeyPair newKeyPair() {

		try {

			KeyPairGenerator keygen = KeyPairGenerator.getInstance(ALGORITHM);
			keygen.initialize(1024, SecureRandom.getInstance("SHA1PRNG"));
			KeyPair keyPair = keygen.generateKeyPair();

			return keyPair;
		}
		catch (Exception e) {

			throw new IllegalStateException(e);
		}
	}

	/**
	 * キーペアを取得します。
	 *
	 * @return キーペア。
	 */
	protected KeyPair getKeyPair() {

		return this.keyPair;
	}

	/**
	 * キーペアを設定します。
	 *
	 * @param keyPair キーペア。
	 */
	protected void setKeyPair(KeyPair keyPair) {

		this.keyPair = keyPair;
	}

	/**
	 * 16進数文字列形式の公開鍵を取得します。
	 *
	 * @return 公開鍵。
	 */
	public String getPublickeyAsHex() {

		return new Hex(this.getPublickey().getEncoded()).toString();
	}

	/**
	 * 公開鍵を取得します。
	 *
	 * @return 公開鍵。
	 */
	protected PublicKey getPublickey() {

		if (this.publickey != null) {

			return this.publickey;
		}

		return this.getKeyPair().getPublic();
	}

	/**
	 * 公開鍵を設定します。
	 *
	 * @param publickey 公開鍵。
	 */
	protected void setPublickey(PublicKey publickey) {

		this.publickey = publickey;
	}

	/**
	 * 公開鍵を設定します。
	 *
	 * @param publickey 16進数文字列形式の公開鍵。
	 */
	protected void setPublickey(String publickey) {

		try {

			this.setPublickey(KeyFactory.getInstance(
				ALGORITHM).generatePublic(
				new X509EncodedKeySpec(new Hex(publickey).toBytes())));
		}
		catch (Exception e) {

			throw new IllegalArgumentException(e);
		}
	}
}
